const path = require('path')
const fs = require('fs')
const utils = require('./utils')
const dict_config_keys = require('./dict_config_keys')
const packageInfo = require('../package.json')

const FISHDICT_FILE_ID = '__FISHDICT__'

class Dict {
    constructor(file, options) {
        this.file_ = file;
        this.config_ = null;
        this.options_ = options

        /*
            [
                {
                    word: ''
                    offset: 0,
                    length: 100
                }
            ]
        */
        this.index_ = null;

        // file handler
        this.hDictFile_ = null;
        this.dictResourceHeaderInfo_ = null;
        this.dictAssetsHeaderBeginOffst = -1;
        this.dictAssetsHeaderInfo_ = null;

        /**
         * {
         *      template:
         *      words: [
         *          {
         *              from: 'x',
         *              to: 'y'
         *          }
         *      ]
         * }
         */
        this.redirect_ = []

        this.replace_ = []

        this.content_ = null;

        this.hasJs_ = -1;
        this.hasCss_ = -1;

        // read dict
        this.init();
    }

    async init() {
        const now = new Date().getTime();
        try {
            this.hDictFile_ = fs.openSync(this.file_, 'r');
            if (!this.hDictFile_) return;
            let offset = 0;
            const tempBuf = Buffer.alloc(FISHDICT_FILE_ID.length, 0);
            if (fs.readSync(this.hDictFile_, tempBuf) != tempBuf.length) return;
            if (tempBuf.toString('utf-8') != FISHDICT_FILE_ID) return;
            offset += tempBuf.length;

            const versionBuf = Buffer.alloc(4, 0)
            if (fs.readSync(this.hDictFile_, versionBuf) != versionBuf.length) return;
            if (versionBuf.readUint32LE() != packageInfo.dict_version) return;
            offset += 4;

            const configStr = await this.readFileInfo('resource/config.json', true)

            this.config_ = JSON.parse(configStr)

            await this.readIndex();

            // 讀取replace
            await this.readRedirect();

            await this.readReplace();
        } catch (e) {
            console.log('..init Dict exception', e)
        }
        console.log('...parse time:', new Date().getTime() - now, this.index_.length)
    }

    async readFileInfo(filePath, isStr, offset, len) {
        const isResource = filePath.startsWith('resource/');
        const isAssets = filePath.startsWith('assets/');
        if (isResource) {
            await this.readResourceHeader();
        } else if (isAssets) {
            await this.readAssetsHeader();
        }

        if (!isAssets && !isResource) return null;

        const coreFilePathPos = filePath.indexOf('/')
        if (coreFilePathPos == -1) {
            return null;
        }
        const coreFilePath = filePath.substring(coreFilePathPos + 1)
        const headerInfo = isResource ? this.dictResourceHeaderInfo_ : this.dictAssetsHeaderInfo_;
        const foundInfo = headerInfo[coreFilePath.toLowerCase()];
        if (!foundInfo) return null;

        const r_offset = offset || 0
        const r_len = len || foundInfo.length
        let buf = Buffer.alloc(r_len, 0);
        if (filePath == 'resource/content.data') {
            if (!this.content_) {
                const temp = Buffer.alloc(foundInfo.length, 0);
                if (fs.readSync(this.hDictFile_, temp, 0, foundInfo.length, foundInfo.offset + r_offset) != foundInfo.length) return null;
                this.content_ = await utils.unzipBuffer(temp);
            }
            this.content_.copy(buf, 0, r_offset, r_len + r_offset)
        } else {
            if (fs.readSync(this.hDictFile_, buf, 0, r_len, foundInfo.offset + r_offset) != r_len) return null
            buf = await utils.unzipBuffer(buf);
            if (isStr) return buf.toString('utf-8')
        }

        return buf;
    }

    async readResourceHeader() {
        if (this.dictResourceHeaderInfo_) return;

        const { items, offset } = await this.readDictHeaderCore(FISHDICT_FILE_ID.length + 4)
        this.dictAssetsHeaderBeginOffst = offset
        this.dictResourceHeaderInfo_ = items

        console.log('..this.dictResourceHeaderInfo_', this.dictResourceHeaderInfo_)
    }
    async readAssetsHeader() {
        if (this.dictAssetsHeaderInfo_) return
        const { items, offset } = await this.readDictHeaderCore(this.dictAssetsHeaderBeginOffst)
        this.dictAssetsHeaderInfo_ = items
    }

    async readDictHeaderCore(offset) {
        const lenBuf = Buffer.alloc(4, 0);
        if (fs.readSync(this.hDictFile_, lenBuf, 0, 4, offset) != 4) return;
        offset += 4;

        const headLen = lenBuf.readUint32LE();

        let headBuf = Buffer.alloc(headLen, 0);
        if (fs.readSync(this.hDictFile_, headBuf, 0, headLen, offset) != headLen) return;
        offset += headLen;
        headBuf = await utils.unzipBuffer(headBuf)

        let headerReadPos = 0;
        const items = new Map();
        let contentOffset = 0;
        while (headerReadPos < headBuf.length) {
            const oneLen = headBuf.readUint16LE(headerReadPos);
            headerReadPos += 2;
            const oneSize = headBuf.readUint32LE(headerReadPos);
            headerReadPos += 4
            const name = headBuf.toString('utf-8', headerReadPos, headerReadPos + oneLen - 4)
            headerReadPos += oneLen - 4

            const item = {
                offset: contentOffset + offset,
                length: oneSize
            }
            items[name] = item;
            contentOffset += oneSize
        }

        return {
            items: items,
            offset: contentOffset + offset
        };
    }

    /**
     * 
     * @param {*} filePath e.g. assets/a.png
     * @param {*} isStr 
     */
    async readAssetsFile(filePath, isStr) {
        // if (this.options_ && this.options_.debug && this.options_.debugDir) {
        //     const file_path = path.join(this.options_.debugDir, filePath);
        //     if (!fs.existsSync(file_path)) return null;
        //     const buf = fs.readFileSync(file_path);
        //     if (isStr) return buf.toString('utf-8')
        //     return buf;
        // }
        return await this.readFileInfo(filePath, isStr);
    }

    async readReplace() {
        let str = await this.readAssetsFile('resource/replace.txt', true)
        if (!str) return;
        str = str.replace(/\r\n/g, '\n')
        const arr = str.split('\n')
        this.replace_ = []
        arr && arr.forEach((line) => {
            const pos = line.indexOf(' ')
            if (pos >= 0) {
                const key = line.substring(0, pos)
                const value = line.substring(pos + 1).trim()
                if (value) this.replace_.push({ key, value })
            }
        })
    }

    async readRedirect() {
        const str_redirect_files = this.getConfig('redirect')
        if (str_redirect_files) {
            const files = str_redirect_files.split(',')
            for (let i = 0; files && i < files.length; ++i) {
                const item = files[i]
                let str = await this.readAssetsFile(path.join('resource', item.trim()), true)
                if (!str) return;
                const redirect_item = {
                    template: '',
                    words: []
                }
                str = str.replace(/\r\n/g, '\n')
                const items = str.split('\n')
                items && items.forEach((kv) => {
                    const kv_item = kv.split('\t')
                    if (kv_item) {
                        if (kv_item[0] == '__TEMPLATE__') {
                            redirect_item.template = kv.substring(kv_item[0].length + 1)
                        } else if (kv_item.length == 2) {
                            const kv_key = kv_item[0] && kv_item[0].trim()
                            const kv_value = kv_item[1] && kv_item[1].trim()
                            kv_key && kv_value && redirect_item.words.push({
                                from: kv_key,
                                to: kv_value
                            })
                        }
                    }
                })
                if (redirect_item.words && redirect_item.words.length) {
                    this.redirect_.push(redirect_item)
                }
            }
        }
    }

    destroy() {
        this.index_ = null;
        this.hDictFile_ && fs.closeSync(this.hDictFile_)
        this.hDictFile_ = null;
        this.config_ = null;
        this.content_ = null;
    }

    async readIndex() {
        const allIndexBuf = await this.readFileInfo('resource/index.data', false)
        let offset = 0;
        this.index_ = []

        let contentOffset = 0;
        const charset = this.config_['charset'] || 'utf-8'
        console.log('..charset', charset)
        while (offset < allIndexBuf.length) {
            const indexLen = allIndexBuf.readUint16LE(offset)

            const contentLen = allIndexBuf.readUint32LE(offset + 2)

            const word = allIndexBuf.toString(charset, offset + 6, indexLen - 4 + offset + 6)

            const newItem = {
                word,
                offset: contentOffset,
                length: contentLen
            }

            contentOffset += contentLen;
            this.index_.push(newItem);

            offset += indexLen + 2
        }
    }

    // check
    isValid() {
        // id
        if (!this.getConfig(dict_config_keys.id)) return false

        // version
        if (!this.getConfig(dict_config_keys.version)) return false

        // name
        if (!this.getConfig(dict_config_keys.name)) return false

        // index
        if (!this.index_ || !this.index_.length) return false

        // content
        if (!this.hDictFile_) return false

        if (!this.dictAssetsHeaderInfo_) return false;

        return true;
    }

    getConfig(key) {
        if (!key) return this.config_;
        return this.config_ && this.config_[key]
    }

    getAllConfig() {
        return this.config_;
    }

    getWordCount() {
        return (this.index_ && this.index_.length) || 0;
    }

    /**
     * 
     * @param {*} options readContent, maxMatchCount
     * @returns 
     *          [
     *              {
     *                  index:
     *                  word:
     *                  content:
     *              }
     *          ]
     */
    async findInWord(strOrReg, options) {
        options = options || {}
        if (!this.index_ || !this.hDictFile_) return null;
        const tempMaxMatchCount = options.maxMatchCount || this.getWordCount();

        let ret = []

        // reg
        if (Object.prototype.toString.call(strOrReg) === '[object RegExp]') {
            for (let i = 0; i < this.index_.length; ++i) {
                const item = this.index_[i];
                if (strOrReg.test(item.word)) {
                    await this.addResultItem(i, ret, options.readContent);
                }
                if (ret.length >= tempMaxMatchCount) {
                    break;
                }
            }
        }
        // index
        else if (Number(strOrReg)) {
            const index = Number(strOrReg) - 1;
            await this.addResultItem(index, ret, options.readContent)
        }
        // str
        else {
            let to_search = strOrReg;
            for (let i = 0; i < this.index_.length; ++i) {
                const item = this.index_[i];
                if (item.word.indexOf(to_search) == 0 ||
                    to_search.indexOf(item.word) == 0) {
                    await this.addResultItem(i, ret, options.readContent);
                }
                if (ret.length >= tempMaxMatchCount) {
                    break;
                }
            }

            const redirect_result = this.findRedirect(strOrReg);
            if (redirect_result) {
                to_search = redirect_result.to
                for (let i = 0; i < this.index_.length; ++i) {
                    const item = this.index_[i];
                    if (item.word.indexOf(to_search) == 0 ||
                        to_search.indexOf(item.word) == 0) {
                        // 去重
                        const found = ret.find(fri => fri.index == i + 1)
                        if (!found) await this.addResultItem(i, ret, options.readContent, redirect_result);
                    }
                    if (ret.length >= tempMaxMatchCount) {
                        break;
                    }
                }
            }

            // 排序
            ret = ret.sort((a, b) => {
                if (a.word == strOrReg) return -1;
                if (b.word == strOrReg) return 1;
                const pos_a = a.word.indexOf(strOrReg);
                const pos_b = b.word.indexOf(strOrReg);
                if (pos_a < pos_b) return -1;
                if (pos_a > pos_b) return 1;
                return a.word.length - b.word.length
            })
        }


        return ret;
    }

    findRedirect(str) {
        for (let i = 0; i < this.redirect_.length; ++i) {
            const redirect_item = this.redirect_[i];
            const foundItem = redirect_item.words.find((item) => {
                return str.startsWith(item.from)
            })
            if (foundItem) {
                return {
                    template: redirect_item.template.replace(/\$\{1\}/g, foundItem.from).replace(/\$\{2\}/g, foundItem.to),
                    ...foundItem
                }
            }
        }

        for (let i = 0; i < this.redirect_.length; ++i) {
            const redirect_item = this.redirect_[i];
            const foundItem = redirect_item.words.find((item) => {
                return item.from.startsWith(str)
            })
            if (foundItem) {
                return {
                    template: redirect_item.template.replace(/\$\{1\}/g, foundItem.from).replace(/\$\{2\}/g, foundItem.to),
                    ...foundItem
                }
            }
        }
    }

    findInContent(strOrReg) {
        throw "Unimplemented"
    }

    async addResultItem(i, ret, readContent, redirect_result) {
        const indexInfo = this.index_[i]
        if (!indexInfo) {
            ret.push({
                fromWord: (redirect_result && redirect_result.from) || i + 1,
                index: i + 1,
                content: `${i + 1}未收錄，辭典搜尋範圍為[${1}, ${this.getWordCount()}]`
            });
            return;
        }

        let content = readContent ? await this.readContentByIndex(indexInfo) : ''
        if (redirect_result) {
            content = redirect_result.template + '\r\n  ' + content;
        }
        content = this.replaceContent(content)

        if (this.hasCss()) {
            content += '\n<link href="assets/index.css" rel="stylesheet">'
        }

        ret.push({
            ...indexInfo,
            fromWord: (redirect_result && redirect_result.from) || indexInfo.word,
            index: i + 1,
            content,
            js: this.hasJs() ? 'assets/index.js' : undefined
        });
    }

    hasCss() {
        if (this.hasCss_ == -1) {
            this.hasCss_ = !!this.readAssetsFile('assets/index.css')
        }

        return this.hasCss_;
    }

    hasJs() {
        if (this.hasJs_ == -1) {
            this.hasJs_ = !!this.readAssetsFile('assets/index.js')
        }

        return this.hasJs_;
    }

    replaceContent(c) {
        let r = c;
        for (let i = this.replace_.length - 1; i >= 0; --i) {
            const t = this.replace_[i]
            r = r.replaceAll(t.key, t.value)
        }
        return r;
    }

    async readContentByIndex(indexInfo) {
        if (!this.hDictFile_ || !indexInfo) return null;
        if (indexInfo.content) return indexInfo.content;

        const buf = await this.readFileInfo('resource/content.data', false, indexInfo.offset, indexInfo.length);
        if (!buf) return null;

        // unzip
        indexInfo.content = buf.toString(this.config_['charset'] || 'utf-8')
        return indexInfo.content;
    }
}

module.exports = Dict;